﻿/*
   Copyright 2012 University of Southampton

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using uk.ac.soton.ses.DAL;

// Some notes on how we determine file changes:
//      Created:    No file with same user, same hash and same file size exists OR a file with same filename+basepath is not found
//      Moved:      Same file name, same user, same hash, same file size but base path has changed
//      Renamed     Old file is deleted, user is the same, hash is the same, size is the same

//We will use the following properties for identifying file changes:
//datafile.BasePath
//datafile.CreationTime
//datafile.Filename
//datafile.FileSize
//datafile.LastAccessTime
//datafile.LastWriteTime
//datafile.SHA1Hash
//datafile.SHA1HashGeneratedTime    : SHA1Hash is the only thing that matters for matching, but if time stamps match we can skip generating the hash everytime for the file system

//We don't care about the following properties for matching:
//datafile.Description              : This property is not available in the file system (might need to use this for the XML file but not in this version)
//datafile.ExperimentDatatypeID     : This property is not available in the file system (might need to use this for the XML file but not in this version)
//datafile.ExperimentID             : This property is not available in the file system (might need to use this for the XML file but not in this version)
//datafile.ID                       : This property is not available in the file system (might need to use this for the XML file but not in this version)

namespace uk.ac.soton.ses
{
    public class ExperimentMetadataContainerComparer
    {
        //Fields
        private bool _wasRenamed = false;
        private bool _wasMoved = false;

        //Compare settings
        private CompareSettings _changedContentsCompareSettings = null;
        private CompareSettings _renamedCompareSettings = null;
        private CompareSettings _unchangedCompareSettings = null;
        private CompareSettings _movedCompareSettings = null;
        private CompareSettings _movedAndRenamedCompareSettings = null;
        private CompareSettings _timeStampChangedCompareSettings = null;
            
        private ExperimentMetadataContainer _savedMetadata = null;
        private ExperimentMetadataContainer _newMetadata = null;
        private static long _timeStampTolerance = 40000;

        private List<ChangedExperimentDataFileBO> _changedContentsFiles = null;
        private List<ChangedExperimentDataFileBO> _unchangedFiles = null;
        private List<ChangedExperimentDataFileBO> _changedTimeStampsFiles = null;
        private List<ChangedExperimentDataFileBO> _renamedFiles = null;
        private List<ChangedExperimentDataFileBO> _movedFiles = null;
        private List<ChangedExperimentDataFileBO> _movedAndRenamedFiles = null;
        private List<ExperimentDataFileMetadataContainer> _createdFiles = null;
        private List<ExperimentDataFileMetadataContainer> _missingFiles = null;

        //Properties
        public ExperimentMetadataContainer SavedMetadata { get { return this._savedMetadata; } }
        public ExperimentMetadataContainer NewMetadata { get { return this._newMetadata; } }
        //public IEnumerable<ExperimentDataFileBO> FilesDeleted { get { return this._deletedFiles; } }
        //public IEnumerable<ExperimentDataFileBO> FilesAdded { get { return this._createdFiles; } }
        public IEnumerable<ChangedExperimentDataFileBO> FilesMoved { get { return this._movedFiles; } }
        public IEnumerable<ChangedExperimentDataFileBO> FilesRenamed { get { return this._renamedFiles; } }
        public IEnumerable<ChangedExperimentDataFileBO> FilesMovedAndRenamed { get { return this._movedAndRenamedFiles; } }
        public List<ChangedExperimentDataFileBO> UnchangedFiles { get { return this._unchangedFiles; } }
        public List<ChangedExperimentDataFileBO> ChangedTimeStampsFiles { get { return this._changedTimeStampsFiles; } }
        public List<ChangedExperimentDataFileBO> ChangedFiles { get { return this._changedContentsFiles; } }
        public List<ExperimentDataFileMetadataContainer> MissingFiles { get { return this._missingFiles; } }
        public List<ExperimentDataFileMetadataContainer> CreatedFiles { get { return this._createdFiles; } }

        /// <summary>
        /// Compares the name of the experiment folder with the saved metadata to check for a rename
        /// </summary>
        public bool WasRenamed
        {
            get
            {
                return this._wasRenamed;
            }
        }

        public bool WasMoved
        {
            get
            {
                return this._wasMoved;
            }
        }

        public ExperimentMetadataContainerComparer(ExperimentMetadataContainer savedMetadata, ExperimentMetadataContainer newMetadata)
        {
            //Save data
            this._savedMetadata = savedMetadata;
            this._newMetadata = newMetadata;

            //Set flags that are used for matching
            this.SetCompareFlags();

            this.Refresh();
        }

        public void Refresh()
        {
            //Record properties
            this._wasRenamed = Path.GetFileName(this._savedMetadata.Experiment.Name) != Path.GetFileName(this._newMetadata.Experiment.Name);
            this._wasMoved = !this._savedMetadata.Experiment.DefaultBasePath.Equals(this._newMetadata.Experiment.DefaultBasePath, StringComparison.OrdinalIgnoreCase);

            //We need two lists containing all the files which, when we have removed files that have been identified as renamed, moved, etc., will contain created or deleted files
            this._createdFiles = this._newMetadata.ExperimentDataFiles.Select(df => df.Copy()).ToList();
            this._missingFiles = this._savedMetadata.ExperimentDataFiles.Select(df => df.Copy()).ToList();

            //Populate the lists using the appropriate flags
            this._changedTimeStampsFiles = this.FindFiles(this._timeStampChangedCompareSettings, true);//We'll find files with any property changed before unchanged files gets a chance to remove the files it finds from the lists
            this._unchangedFiles = this.FindFiles(this._unchangedCompareSettings, true);          //Remove all files that haven't changed from our file list so they don't get mistaken for a rename, move or anything else
            this._renamedFiles = this.FindFiles(this._renamedCompareSettings, true);              //Next, look for renamed files and remove them from our overall list
            this._movedFiles = this.FindFiles(this._movedCompareSettings, true);                  //Next, look for files that have moved
            this._movedAndRenamedFiles = this.FindFiles(this._movedAndRenamedCompareSettings, true);      //Next, look for files that have moved and also renamed
            this._changedContentsFiles = this.FindFiles(this._changedContentsCompareSettings, true);      //Checked for changed files

            //We should be left with a list of files considered missing or new
        }

        private void SetCompareFlags()
        {
            //---Changed files---
            //basepath same
            //filename same
            //owner same
            //filesize, sha1hash changed
            this._changedContentsCompareSettings = new CompareSettings()
            {
                UnchangedProperties_MatchAll = CompareFlags.BasePath | CompareFlags.RemainingPath | CompareFlags.Owner,
                ChangedProperties_MatchAll = CompareFlags.None,
                UnchangedProperties_MatchAtLeastOne = CompareFlags.None,
                ChangedProperties_MatchAtLeastOne = CompareFlags.FileSize | CompareFlags.SHA1Hash | CompareFlags.LastWriteTime | CompareFlags.LastAccessTime,
                MultipleMatchesPermitted = false,
                ChangedExperimentDataFileType = ChangedExperimentDataFileType.ContentsChanged,
                FileSizeGreaterThan = -1 //Empty files that are in the same place we're okay with, as users may do this
            };

            //---Moved files---
            //basepath changed
            //filesize same
            //sha1hash same
            //owner same?
            //ignoring time stamps
            this._movedCompareSettings = new CompareSettings()
            {
                UnchangedProperties_MatchAll = CompareFlags.ObjectName | CompareFlags.FileSize | CompareFlags.SHA1Hash | CompareFlags.Owner,
                ChangedProperties_MatchAll = CompareFlags.ObjectFolder,
                UnchangedProperties_MatchAtLeastOne = CompareFlags.None,
                ChangedProperties_MatchAtLeastOne = CompareFlags.None,
                MultipleMatchesPermitted = false,
                ChangedExperimentDataFileType = ChangedExperimentDataFileType.Moved,
                FileSizeGreaterThan = 0
            };

            //---Moved files---
            //basepath changed
            //filesize same
            //sha1hash same
            //owner same?
            //ignoring filename as the file might have been renamed when moved
            //ignoring time stamps
            this._movedAndRenamedCompareSettings = new CompareSettings()
            {
                UnchangedProperties_MatchAll = CompareFlags.FileSize | CompareFlags.SHA1Hash | CompareFlags.Owner,
                ChangedProperties_MatchAll = CompareFlags.ObjectFolder,
                UnchangedProperties_MatchAtLeastOne = CompareFlags.None,
                ChangedProperties_MatchAtLeastOne = CompareFlags.None,
                MultipleMatchesPermitted = false,
                ChangedExperimentDataFileType = ChangedExperimentDataFileType.Moved,
                FileSizeGreaterThan = 0
            };

            //---Unchanged files---
            //basepath the same
            //filesize the same
            //sha1hash the same
            //filename the same
            //ignoring time stamps
            this._unchangedCompareSettings = new CompareSettings()
            {
                UnchangedProperties_MatchAll = CompareFlags.BasePath | CompareFlags.RemainingPath | CompareFlags.FileSize | CompareFlags.SHA1Hash,
                ChangedProperties_MatchAll = CompareFlags.None,
                UnchangedProperties_MatchAtLeastOne = CompareFlags.None,
                ChangedProperties_MatchAtLeastOne = CompareFlags.None,
                MultipleMatchesPermitted = false,
                ChangedExperimentDataFileType = ChangedExperimentDataFileType.NotChanged,
                FileSizeGreaterThan = -1 //Empty files that are in the same place we're okay with, as users may do this
            };

            //---Time stamp changed---
            //basepath the same
            //filesize the same
            //sha1hash the same
            //filename the same
            //one time stamp changed
            this._timeStampChangedCompareSettings = new CompareSettings()
            {
                UnchangedProperties_MatchAll = CompareFlags.BasePath | CompareFlags.RemainingPath | CompareFlags.FileSize | CompareFlags.SHA1Hash,
                ChangedProperties_MatchAll = CompareFlags.None,
                UnchangedProperties_MatchAtLeastOne = CompareFlags.None,
                ChangedProperties_MatchAtLeastOne = CompareFlags.LastAccessTime | CompareFlags.LastWriteTime | CompareFlags.CreationTime | CompareFlags.SHA1HashGeneratedTime,
                MultipleMatchesPermitted = false,
                ChangedExperimentDataFileType = ChangedExperimentDataFileType.TimeStampChanged,
                FileSizeGreaterThan = -1 //Empty files that are in the same place we're okay with, as users may do this
            };
            

            //---Renamed files---
            //basepath the same
            //filesize the same
            //sha1hash the same
            //filename changed
            //ignoring time stamps
            this._renamedCompareSettings = new CompareSettings()
            {
                UnchangedProperties_MatchAll = CompareFlags.ObjectFolder | CompareFlags.FileSize | CompareFlags.SHA1Hash | CompareFlags.Owner,
                ChangedProperties_MatchAll = CompareFlags.ObjectName,
                UnchangedProperties_MatchAtLeastOne = CompareFlags.None,
                ChangedProperties_MatchAtLeastOne = CompareFlags.None,
                MultipleMatchesPermitted = false,
                ChangedExperimentDataFileType = ChangedExperimentDataFileType.Renamed,
                FileSizeGreaterThan = 0
            };
        }

        private List<ChangedExperimentDataFileBO> FindFilesMatchingFlags(CompareSettings allCompareFlags, bool removeFromExistingLists)
        {
            CompareFlags unchangedProperties_MatchAllFlags = allCompareFlags.UnchangedProperties_MatchAll;
            CompareFlags changedProperties_MatchAllFlags = allCompareFlags.ChangedProperties_MatchAll;
            CompareFlags unchangedProperties_MatchAtLeastOneFlag = allCompareFlags.UnchangedProperties_MatchAtLeastOne;
            CompareFlags changedProperties_MatchAtLeastOneFlag = allCompareFlags.ChangedProperties_MatchAtLeastOne;
            bool multipleMatchesPermitted = allCompareFlags.MultipleMatchesPermitted;

            List<ChangedExperimentDataFileBO> filesFound = new List<ChangedExperimentDataFileBO>();

            //Iterate over all of the files considered as missing and try and see if any of them qualify
            foreach (ExperimentDataFileMetadataContainer savedFile in this._missingFiles)
            {
                //Keep a list of files the same as this file, so we can ensure that we count how many were found
                List<ChangedExperimentDataFileBO> savedFileMatches = new List<ChangedExperimentDataFileBO>();

                //Iterate around all of the files considered as created looking for renamed files
                foreach (ExperimentDataFileMetadataContainer newFile in this._createdFiles)
                {
                    CompareFlags thisFileUnchangedProperties = this.GetMatchFlags(savedFile.ExperimentDataFile, newFile.ExperimentDataFile);
                    bool matching = true;

                    //Check to ensure all of the requested properties have not been changed
                    matching &= (thisFileUnchangedProperties & unchangedProperties_MatchAllFlags) == unchangedProperties_MatchAllFlags;
                    //Check to ensure none of the unchanged flags are in the list of properties that should have changed
                    matching &= (thisFileUnchangedProperties & changedProperties_MatchAllFlags) == 0;
                    //Check for at least one unchanged property, only when we were passed flags
                    matching &= (unchangedProperties_MatchAtLeastOneFlag == CompareFlags.None || (thisFileUnchangedProperties & unchangedProperties_MatchAtLeastOneFlag) > 0);
                    //Check for at least one changed property, only when we were passed flags
                    matching &= (changedProperties_MatchAtLeastOneFlag == CompareFlags.None || (changedProperties_MatchAtLeastOneFlag & (thisFileUnchangedProperties ^ CompareFlags.All)) > 0);
                    //check for size
                    matching &= (newFile.ExperimentDataFile.FileSize > allCompareFlags.FileSizeGreaterThan);

                    //If we found a matching file, save it and keep going
                    if (matching)
                    {
                        savedFileMatches.Add(new ChangedExperimentDataFileBO(newFile, savedFile, allCompareFlags.ChangedExperimentDataFileType, thisFileUnchangedProperties));
                    }
                }

                if (!multipleMatchesPermitted && savedFileMatches.Count > 1)
                {
                    string message = string.Format("Found more than one possible file matching {0}. Can't process this any further. Moving onto next file.", savedFile.ExperimentDataFile.GetFullPath());
                    HDCFileSystemWatcher.Message(message, System.Diagnostics.TraceLevel.Error);
                }
                else
                {
                    //Add the file to the list that will be returned
                    filesFound.AddRange(savedFileMatches);

                    //If we are allowed to remove from the _createdFiles list, then do it now to save us revisiting files we've already found a match for
                    if (removeFromExistingLists)
                    {
                        foreach (ChangedExperimentDataFileBO filefound in savedFileMatches)
                        {
                            this._createdFiles.Remove(filefound.NewDataFile);
                        }
                    }
                }
            }
            return filesFound;
        }

        private List<ChangedExperimentDataFileBO> FindFiles(CompareSettings compareSettings, bool removeFromExistingLists)
        {
            List<ChangedExperimentDataFileBO> filesFoundList = this.FindFilesMatchingFlags(compareSettings, removeFromExistingLists);
            if (removeFromExistingLists)
            {
                foreach (ChangedExperimentDataFileBO filefound in filesFoundList)
                {
                    //We only need to remove from _missingFiles (because FindFilesMatchingFlags did _createdFiles)
                    //this._createdFiles.Remove(filefound.NewDataFile);
                    this._missingFiles.Remove(filefound.SavedDataFile);
                }
            }
            return filesFoundList;
        }

        /// <summary>
        /// Given two file business objects, we compare each property and set a flag if it matches
        /// </summary>
        /// <param name="file1">An ExperimentDataFileBO to match against</param>
        /// <param name="file2">An ExperimentDataFileBO to match against</param>
        /// <returns></returns>
        private CompareFlags GetMatchFlags(ExperimentDataFileBO file1, ExperimentDataFileBO file2)
        {
            CompareFlags matchFlags = CompareFlags.None;
            matchFlags |= TimesEqual(file1.LastAccessTime, file2.LastAccessTime) ? CompareFlags.LastAccessTime : 0;
            matchFlags |= TimesEqual(file1.LastWriteTime, file2.LastWriteTime) ? CompareFlags.LastWriteTime : 0; // (file1.LastWriteTime == file2.LastWriteTime ? CompareFlags.LastWriteTime : 0);
            matchFlags |= TimesEqual(file1.CreationTime, file2.CreationTime) ? CompareFlags.CreationTime : 0; //(file1.CreationTime == file2.CreationTime ? CompareFlags.CreationTime : 0);
            matchFlags |= TimesEqual(file1.SHA1HashGeneratedTime, file2.SHA1HashGeneratedTime) ? CompareFlags.SHA1HashGeneratedTime : 0;
            matchFlags |= (file1.SHA1Hash == file2.SHA1Hash ? CompareFlags.SHA1Hash : 0);
            matchFlags |= (file1.FileSize == file2.FileSize ? CompareFlags.FileSize : 0);
            matchFlags |= (file1.RemainingPath.Equals(file2.RemainingPath,StringComparison.OrdinalIgnoreCase) ? CompareFlags.RemainingPath : 0);
            matchFlags |= (file1.BasePath.Equals(file2.BasePath,StringComparison.OrdinalIgnoreCase) ? CompareFlags.BasePath : 0);
            matchFlags |= (file1.ObjectName.Equals(file2.ObjectName,StringComparison.OrdinalIgnoreCase) ? CompareFlags.ObjectName : 0);
            matchFlags |= (file1.ObjectFolder.Equals(file2.ObjectFolder, StringComparison.OrdinalIgnoreCase) ? CompareFlags.ObjectFolder : 0);
            matchFlags |= (file1.FileSystemUserID == file2.FileSystemUserID ? CompareFlags.Owner : 0);
            return matchFlags;
        }

        public static bool TimesEqual(DateTime time1, DateTime time2)
        {
            //Because we lose accuracy when storing in a datetime column, use a tolerance of 4 milliseconds (SQL server rounds time to 0.000, 0.0003 and 0.0007 seconds)
            //http://msdn.microsoft.com/en-us/library/ms187819.aspx

            //Use Duration() to get absolute value in case time1 is less than time2 and we get a negative difference
            //bool val = (time1 - time2).Duration() < TimeSpan.FromMilliseconds(4);

            return TimesEqual(time1.Ticks - time2.Ticks);
        }

        public static bool TimesEqual(long timediff)
        {
            return Math.Abs(timediff) < _timeStampTolerance;
        }

        public static bool TimeIsLater(DateTime earliertime, DateTime latertime)
        {
            long diff = latertime.Ticks - earliertime.Ticks;
            if (TimesEqual(diff))
            {
                return false;
            }
            else
            {
                return diff > 0;
            }

            //if (TimesEqual(earliertime, latertime))
            //{
            //    return false;
            //}
            //else
            //{
            //    return (latertime - earliertime).Ticks > 0;
            //}
        }

        public void PrintDebugInfo()
        {
            //Debugging code, don't need this left in all the time
            foreach (ChangedExperimentDataFileBO datafile in this.UnchangedFiles)
                HDCFileSystemWatcher.Message(string.Format("Unchanged: {0}", datafile.SavedDataFile.ExperimentDataFile.GetFullPath()), System.Diagnostics.TraceLevel.Verbose);
            foreach (ChangedExperimentDataFileBO datafile in this.FilesRenamed)
                HDCFileSystemWatcher.Message(string.Format("Renamed: {0}=>{1}", datafile.SavedDataFile.ExperimentDataFile.GetFullPath(), datafile.NewDataFile.ExperimentDataFile.GetFullPath()), System.Diagnostics.TraceLevel.Verbose);
            foreach (ChangedExperimentDataFileBO datafile in this.FilesMoved)
                HDCFileSystemWatcher.Message(string.Format("Moved: {0}=>{1}", datafile.SavedDataFile.ExperimentDataFile.GetFullPath(), datafile.NewDataFile.ExperimentDataFile.GetFullPath()), System.Diagnostics.TraceLevel.Verbose);
            foreach (ChangedExperimentDataFileBO datafile in this.FilesMovedAndRenamed)
                HDCFileSystemWatcher.Message(string.Format("Moved+renamed: {0}=>{1}", datafile.SavedDataFile.ExperimentDataFile.GetFullPath(), datafile.NewDataFile.ExperimentDataFile.GetFullPath()), System.Diagnostics.TraceLevel.Verbose);
            foreach (ChangedExperimentDataFileBO datafile in this.ChangedFiles)
                HDCFileSystemWatcher.Message(string.Format("Changed: {0} (Same:{1}, Changed:{2})", datafile.SavedDataFile.ExperimentDataFile.GetFullPath(), datafile.MatchingFlags, datafile.MatchingFlags ^ CompareFlags.All), System.Diagnostics.TraceLevel.Verbose);
            foreach (ChangedExperimentDataFileBO datafile in this.ChangedTimeStampsFiles)
                HDCFileSystemWatcher.Message(string.Format("Time stamp different only: {0}, {1}", datafile.SavedDataFile.ExperimentDataFile.GetFullPath(), datafile.ChangedFlags), System.Diagnostics.TraceLevel.Verbose);
            foreach (ExperimentDataFileMetadataContainer datafile in this.CreatedFiles)
                HDCFileSystemWatcher.Message(string.Format("Created: {0}", datafile.ExperimentDataFile.GetFullPath()), System.Diagnostics.TraceLevel.Verbose);
            foreach (ExperimentDataFileMetadataContainer datafile in this.MissingFiles)
                HDCFileSystemWatcher.Message(string.Format("Missing: {0}", datafile.ExperimentDataFile.GetFullPath()), System.Diagnostics.TraceLevel.Verbose);
        }
    }
}
